I am new to Spring Security and Oauth2. In my spring boot application, I have implemented authentication with Oauth2 with following set of changes:
Custom Ouath2 User service is as follows:
#Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private UserRepository userRepository;
#Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
...
}
}
Security Configuration is as follows:
#EnableWebSecurity
#Import(SecurityProblemSupport.class)
#ConditionalOnProperty(
value = "myapp.authentication.type",
havingValue = "oauth",
matchIfMissing = true
)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
public SecurityConfiguration(CustomOAuth2UserService customOAuth2UserService) {
this.customOAuth2UserService = customOAuth2UserService;
}
#Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/bundle.js")
.antMatchers("/slds-icons/**")
.antMatchers("/assets/**")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/**")
.antMatchers("/swagger-resources")
.antMatchers("/v2/api-docs")
.antMatchers("/api/redirectToHome")
.antMatchers("/test/**");
}
public void configure(HttpSecurity http) throws Exception {
RequestMatcher csrfRequestMatcher = new RequestMatcher() {
private RegexRequestMatcher requestMatcher =
new RegexRequestMatcher("/api/", null);
#Override
public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
};
http.csrf()
.requireCsrfProtectionMatcher(csrfRequestMatcher)
.and()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/manage/**").permitAll()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.anyRequest().authenticated()//.and().oauth2ResourceServer().jwt()
.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri("/oauth2**")
.and()
.failureUrl("/api/redirectToHome")
.userInfoEndpoint().userService(oauth2UserService())
;
http.cors().disable();
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
return customOAuth2UserService;
}
}
Content of application.properties is as follows:
spring.security.oauth2.client.registration.keycloak.client-id=abcd
spring.security.oauth2.client.registration.keycloak.client-name=Auth Server
spring.security.oauth2.client.registration.keycloak.scope=api
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.client-authentication-method=basic
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
myapp.oauth2.path=https://internal.authprovider.com/oauth2/
spring.security.oauth2.client.provider.keycloak.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.keycloak.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.keycloak.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.keycloak.user-name-attribute=name
myapp.authentication.type=oauth
Now, with the existing authentication mechanism, I would like to add support for multiple authentication providers: LDAP, Form-Login, etc.
In this regard, I have gone through a few articles:
https://www.baeldung.com/spring-security-multiple-auth-providers
Custom Authentication provider with Spring Security and Java Config
But, I am not getting any concrete idea regarding what changes should I do in the existing code base in order to achieve this.
Could anyone please help here? Thanks.
I've created a simplified setup starting from your code with support for both OAuth2 and Basic Auth.
/tenant2/** will start a basic authentication.
/** (everything else) triggers an OAuth2 Authorization Code authentication.
The key to achieve this is to have one #Configuration class per authentication type.
Let's start with the controllers:
Tenant1HomeController
#Controller
public class Tenant1HomeController {
#GetMapping("/tenant1/home")
public String home() {
return "tenant1Home";
}
}
Tenant2HomeController
#Controller
public class Tenant2HomeController {
#GetMapping("/tenant2/home")
public String home() {
return "tenant2Home";
}
}
Now, the configuration classes:
Tenant1SecurityConfiguration
#Configuration
public class Tenant1SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/manage/**").permitAll()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority("ADMIN")
.antMatchers("/tenant1/**").authenticated()
.and()
.oauth2Login()
.and()
.cors()
.disable();
}
}
Tenant2SecurityConfiguration (Notice the #Order(90), that's important
#Order(90)
#Configuration
public class Tenant2SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatcher(new AntPathRequestMatcher("/tenant2/**"))
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/tenant2/**").hasAuthority("BASIC_USER")
.and()
.httpBasic();
http.cors().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password("{noop}password")
.roles("BASIC_USER");
}
}
Finally the configuration:
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: myclient
client-secret: c6dce03e-ea13-4b76-8aab-c876f5c2c1d9
provider:
keycloak:
issuer-uri: http://localhost:8180/auth/realms/myrealm
With this in place, if we hit http://localhost:8080/tenant2/home, will be prompted with the basic auth popup:
Trying with http://localhost:8080/tenant1/home sends you to Keycloak's login form:
UPDATE:
It's completely viable to configure a multitenant application with the configuration above.
The key would be that each authentication provider works with a different set of users (tenants), e.g.:
TENANT 1 (OAuth2 authentication):
#Configuration
public class Tenant1SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.
...
.and()
.oauth2Login()
.and()
...
This first subset of users is federated by the OAuth2 provider, Keycloak in this case.
TENANT 2 (Basic / form /xxx authentication):
#Order(90)
#Configuration
public class Tenant2SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(XXX)
For the second tenant, you can use a userDetailsService that points to a different repository of users (LDAP, database...).
Related
I have seen a lot of posts on the same problem, but no solution worked for me.
I have an API secured with spring security as below.
#EnableWebSecurity
#Component
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired private UserService userService;
public SecurityConfiguration() {
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/actuator/shutdown", "/api/register");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userService)
.passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
I need endpoints /api/register and /actuator/shutdown available without authentication. But, as it turned out, all the endpoints are returning the same 401 status code.
try with this.
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//no authentication needed for these context paths
.authorizeRequests()
.antMatchers("/your Urls that dosen't need security/**").permitAll()
We implemented a similar approach as mentioned by Supun Above,
http
.authorizeRequests()
.antMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
.and().httpBasic()
You can keep 'AUTH_WHITELIST' as below to keep adding multiple Endpoints
private static final String[] AUTH_WHITELIST = {
// -- swagger ui
"/api/register",
"/actuator/shutdown"
};
Agenda: To create an authorization and resource server such that.
Rest clients can authenticate and authorize and use tokens to fetch resources about the user.
this worked. accessing resources defined at /rest/user endpoint is working fine
Web clients can SSO using this authorization server
I tried using #EnableOAuth2Sso and also using #EnableOAuth2Client. Both didn't work.
When using EnableOAuth2Sso redirect to oauth server's login happened but redirection back to the app didn't happen.
Users can directly log into the authorization server and see if they have an account.
this is working but it is skipping authentication and authorization and the page is getting displayed immediately
I have a OAuth server with ResourceConfig and WebSecurityConfig
#Configuration
#EnableWebSecurity
#Order(1)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired private AuthenticationSuccessHandler authenticationSuccessHandler;
#Autowired private AuthenticationFailureHandler authenticationFailureHandler;
#Autowired
#Qualifier("userAccountDetailsService")
UserAccountDetailsService userAccountDetailsService;
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// #formatter:off
httpSecurity
.csrf().disable()
.anonymous().disable()
.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.authorizeRequests()
.antMatchers("/**", "/css/**", "/js/**", "/images/**").permitAll()
.antMatchers("/oauth/token").permitAll()
.antMatchers("/userPage/*").hasAnyRole("USER", "HRADMIN")
.antMatchers("/adminPage/*").hasRole("HRADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.loginProcessingUrl("/login")
.failureHandler(authenticationFailureHandler)
.successHandler(authenticationSuccessHandler)
.and()
.logout()
.logoutUrl("/logout");
// #formatter:on
}
#Autowired
protected void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userAccountDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
#Configuration
#EnableResourceServer
#Order(2)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final String RESOURCE_ID = "my_rest_api";
#Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID).stateless(false);
}
#Override
public void configure(HttpSecurity http) throws Exception {
http.
anonymous().disable()
.authorizeRequests()
.antMatchers("/rest/user/**").authenticated()
.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
}
}
Isn't it possible to combine and use web resource and oauth resources in the same server?
I'm using
Spring Boot: 2.2.0.BUILD-SNAPSHOT and
Spring security-oauth2-autoconfigure: 2.1.3.RELEASE
The whole source is available in github
Authorization and Resource server:
https://github.com/john77eipe/SpringSecurityDrills/tree/master/securestore-oauth
Spring Web client using EnableOAuth2Client:
https://github.com/john77eipe/SpringSecurityDrills/tree/master/securestore-web-resource-1
Spring Web client using EnableOAuth2Sso:
https://github.com/john77eipe/SpringSecurityDrills/tree/master/securestore-web-resource-2
I've been developing Spring Cloud (with Netflix OSS stack) microservices architecture for some time. As you would expect, I've separated authorization server as a stand alone microservice. My front end application uses "password" grant type for user login purposes. However, I'm using "client-credentials" grant type for the rest calls that I make from front-end service to other back-end services. Client-credentials grant type is being used among other back-end services as well. By doing so, I am not able to get who is the actual invoker (currently logged in user) of a request. Is there a way to inject authentication and authorization info of the principal to the token that is being issued in client-credentials grant?
My Authorization Server Config class
#Configuration
#EnableAuthorizationServer
#Order(Ordered.HIGHEST_PRECEDENCE)
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("testclient")
.secret("{noop}testsecret")
.authorizedGrantTypes("authorization_code","password","client_credentials")
.scopes("ui")
.autoApprove(true)
// .accessTokenValiditySeconds(3600)
.and()
.withClient("backend-service")
.secret("{noop}backendsecret")
.authorizedGrantTypes("client_credentials","refresh_token")
.scopes("server")
.autoApprove(true)
}
#Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
endpoints.tokenEnhancer(tokenEnhancer());
endpoints.tokenStore(tokenStore());
}
#Bean
public TokenStore tokenStore() {
//return new JdbcTokenStore(dataSource);
return new InMemoryTokenStore();
}
#Bean
#Primary
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenEnhancer(tokenEnhancer());
tokenServices.setTokenStore(tokenStore());
return tokenServices;
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
Security Config Class
#Configuration
#Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.authorizeRequests()
.antMatchers("/resources/**", "/src/main/webapp/**","/css/**","/images/**").permitAll()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll().and().httpBasic().disable();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/static/**","/resources/**", "/src/main/webapp/**","/css/**","/images/**");
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password("{noop}a1b2c3#").roles("User");
}
}
I've tried to implement a Token Enhancer class to propogate additional data in token. However, I don't think this is the correct and secure way for what I'm trying to achieve.
public class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("customInfo", "testdata");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
return oAuth2AccessToken;
}
}
Your assistance would be appreciated.
If you are using an oauth token generated using Client Credentials then you can not get user information. You can only get source of the request (client).
If want user information across micro services then you have to use password grant type to generate oauth token.
Why following basic security configurations do not apply inMemoryAuthentication() clause?
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.httpBasic()
.and()
.authorizeRequests()
.anyRequest().authenticated();
super.configure(http);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("username").password("password");
super.configure(auth);
}
}
After the application initialization, there is still only default user generated by Spring itself, there is no such user like username.
Do not call super method from void configure(AuthenticationManagerBuilder auth). It sets disableLocalConfigureAuthenticationBldr flag to true that leads to your AuthenticationManagerBuilder being ignored. Finally your void configure(AuthenticationManagerBuilder auth) method should look like this:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("username").password("password").roles("USER");
}
In spring boot 2.x, you will have to implement your own UserDetailsService, as described here and here
Example:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger log = LogManager.getLogger();
#Override
protected void configure(HttpSecurity http) throws Exception {
// Note:
// Use this to enable the tomcat basic authentication (tomcat popup rather than spring login page)
// Note that the CSRf token is disabled for all requests
log.info("Disabling CSRF, enabling basic authentication...");
http
.authorizeRequests()
.antMatchers("/**").authenticated() // These urls are allowed by any authenticated user
.and()
.httpBasic();
http.csrf().disable();
}
#Bean
public UserDetailsService userDetailsService() {
// Get the user credentials from the console (or any other source):
String username = ...
String password = ...
// Set the inMemoryAuthentication object with the given credentials:
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
String encodedPassword = passwordEncoder().encode(password);
manager.createUser(User.withUsername(username).password(encodedPassword).roles("USER").build());
return manager;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
I have a problem regarding on the controllers request from a spring boot application.
I have made a certificate in order to run the app on https. The certificate works fine, it is valid.
My main problem is when i test my methods from the controller through postman they(the url reques) work fine on https and http...it shouldn't work on http. Can someone help on this ?
This is my WebSecurityConfig class:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
public static final String AUTHENTICATED_HEADER_NAME = "Authenticated";
public static final String AUTHENTICATED_TRUE = "true";
public static final String AUTHENTICATED_FALSE = "false";
#Autowired
public void globalUserDetails(final AuthenticationManagerBuilder auth) throws Exception {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
auth.userDetailsService(authenticationManager).passwordEncoder(passwordEncoder);
}
#Override
#Bean(value = "authenticationManagerBean")
public org.springframework.security.authentication.AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Configuration
#Order(1)
public static class HTTPBasicSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
//todo check how we can change the root url of swagger
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/documentation**", "/configuration/**", "/v2/api-docs**", "/swagger-ui.html", "/webjars/**", "/swagger-resources/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
//todo http basic allows access to all urls after login
http
.httpBasic()
.and()
.csrf().disable()
.antMatcher("/api/**")
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest()
.authenticated();
}
}
#Configuration
#Order(2)
public static class FormLoginSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
//todo more investigation is required to check if it is safe to ignore csrf for login
.ignoringAntMatchers("/login")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.permitAll()
.successHandler((httpServletRequest, httpServletResponse, authentication) -> {
httpServletResponse.setHeader(AUTHENTICATED_HEADER_NAME, AUTHENTICATED_TRUE);
})
.failureHandler((httpServletRequest, httpServletResponse, e) -> {
httpServletResponse.setHeader(AUTHENTICATED_HEADER_NAME, AUTHENTICATED_FALSE);
httpServletResponse.setStatus(SC_UNAUTHORIZED);
})
.and()
.logout().permitAll()
.and()
.addFilterBefore(new CorsFilter(), ChannelProcessingFilter.class)
.addFilterBefore(new CsrfHeaderFilter(), CsrfFilter.class);
http.exceptionHandling().authenticationEntryPoint((HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) -> {
if (authException != null) {
response.setStatus(SC_UNAUTHORIZED);
}
});
}
}
#Configuration
#Order(3)
public static class TestClass extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers()
.httpStrictTransportSecurity()
.includeSubDomains(true)
.maxAgeInSeconds(31536000);
}
}
}
and this is my spring boot version
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.2.RELEASE</version>
<relativePath></relativePath>
</parent>
Your question is touching on several points:
you can require clients to require secure channels, by adding the security.require_ssl=true configuration property (see the Spring Boot reference documentation about HTTPS)
or use the following configuration snippet http.requiresChannel().anyRequest().requiresSecure();
you might want to enforce that as well with HSTS in Spring Security
None of the above helped the situation I was in.
I figured out that chrome (postman) was automatically transforming my http requests to https.
On the other browsers http requests didn't worked.