Trying to implement Spring Security based role authorizations in my existing Spring Boot system. I want to restrict access of REST API's as per some user authorizations.
For that I created an SpringConfiguration.java class as below:
SecurityConfiguration.java
public class SecurityConfig_BasicAuth extends WebSecurityConfigurerAdapter {
private static String REALM = "Test";
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("Deb").password("deb#123").roles("ADMIN"); // Line 1
auth.inMemoryAuthentication().withUser("Bob").password("Bob#123").roles("USER"); // Line 2
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests().antMatchers("/checkRoleBaseAuthorization/**")
.hasRole("ADMIN").and() // Line 3
.httpBasic().realmName(REALM)
.authenticationEntryPoint(getBasicAuthEntryPoint()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint() {
return new CustomBasicAuthenticationEntryPoint();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}
}
In the above class, I have defined two users/passwords with authorization roles ADMIN/USER. Now I want to allow only those users that have ADMIN roles.
In case credentials do not match below class will return status 401.
CustomBasicAuthenticationEntryPoint.java
public class CustomBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName() + "");
PrintWriter writer = response.getWriter();
writer.println("HTTP Status 401 : " + authException.getMessage());
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("Test");
super.afterPropertiesSet();
}
}
REST API
#RequestMapping(value = "/checkRoleBaseAuthorization", method = RequestMethod.GET)
public String checkRoleBaseAuthorization() {
return "checkRoleBaseAuthorization() Invoked";
}
My program is working to an extent i.e. without users/passwords the API is not accessible.
But why I'm able to login with Bob/Bob#123 too? It has role "USER",
not "ADMIN" which I have specified in SecurityConfiguration.java (see
line 3).
Related
I am doing basic authentication in spring boot . Also i have a filter which does the header filtering and throws error if i dint pass required values in header . Authentication and header filtering are working fine if implemented separately. But if we implement both , i am getting the same response for both the validations ( filter and basic auth ). My guess is as filter response is generated first , it is getting replaced by the authentication response later.
PS: Used ** in code below to tell the issue location .
Any experts please advice . Thanks
#Slf4j
#Component
#Order(Ordered.HIGHEST_PRECEDENCE+2000)
#WebFilter
public class ValidTenantFilter extends OncePerRequestFilter {
#Autowired
private ClientRepository clientRepository;
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
AntPathMatcher pathMatcher = new AntPathMatcher();
return Constant.TENANT_FILTER_URL_LIST.stream()
.anyMatch(p -> pathMatcher.match(p, request.getServletPath()));
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("Inside Tenant Checker filter for path {} with method {} ",request.getServletPath(),request.getMethod());
if(!this.isValidTenant(request)) {
** response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid "+ Constant.X_COMPANY_ID+" and/or "+Constant.X_OPERATOR_ID+ " are passed. Please validate the request."); **
}
else {
filterChain.doFilter(request, response);
}
}
private boolean isValidTenant(HttpServletRequest request) {
// Getting company id and operator id from the header , earlier we were using the tenant id
String companyId = request.getHeader(Constant.X_COMPANY_ID);
String operatorId=request.getHeader(Constant.X_OPERATOR_ID);
if(StringUtils.isNotEmpty(companyId) && StringUtils.isAlphanumeric(companyId)
&& StringUtils.isNotEmpty(operatorId) && StringUtils.isAlphanumeric(operatorId)) {
Client client = clientRepository.findByIdAndOperatorId(companyId, operatorId);
//Only active clients request are entertained. // PRODUCT FIX
if(client!=null && client.getId()!=null && client.isActive()) {
MDC.put(MDC_CLIENT_ID, client.getId().toString());
TenantContext.setCurrentTenant(client.getId().toString());
return true;
}
}
return false;
}
}
And below is the code for the authentication part :
#Configuration
#EnableWebSecurity
#Slf4j
public class SomeConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationEntryPointImpl authEntryPoint;
#Autowired
private ApplicationClients application;
#Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
return new PropertySourcesPlaceholderConfigurer();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable()
.cors().disable();
//http.requiresChannel().antMatchers("/*").requiresSecure();
http.authorizeRequests()
// .antMatchers("/**").hasRole("ADMIN")
//.antMatchers("/user").hasAnyRole("ADMIN")
.anyRequest().authenticated()
.and().httpBasic();
http.headers().defaultsDisabled().cacheControl().and().contentTypeOptions()
.and().frameOptions().deny().xssProtection().block(false)
.and().httpStrictTransportSecurity().includeSubDomains(true).maxAgeInSeconds(31536000);
// Use AuthenticationEntryPoint to authenticate user/password
http.httpBasic().authenticationEntryPoint(authEntryPoint);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources",
"/swagger-resources/configuration/**", "/swagger-ui.html", "/webjars/**");
}
#Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(inMemoryUserDetailsManager());
}
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
final InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
log.info("Importing {} clients:", application.getClients().size());
application.getClients().forEach(client -> {
String encrytedPassword = this.passwordEncoder().encode(client.getPassword());
manager.createUser(User.withUsername(client.getUsername()).password(encrytedPassword).roles(client.getRoles()).build());
log.info("Imported client {}", client.toString());
});
return manager;
}
}
Code for authentication entry point :
#Component
public class AuthenticationEntryPointImpl extends BasicAuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
throws IOException, ServletException {
//This is invoked when a user tries to access a secured REST resource without supplying any credentials
//We should just add a 401 Unauthorized response because there is no 'login page' to redirect to
** response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getOutputStream().println("{\"status\": " + HttpServletResponse.SC_UNAUTHORIZED + ", \"message\": \"" + authEx.getMessage() + "\" }");**
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("api-services");
super.afterPropertiesSet();
}
}
Getting below Response in POSTMAN :
1. If i dont pass any credentials in postman i am getting below response
{
"status": 401,
"message": "Full authentication is required to access this resource"
}
If i dont pass the headers , ideally i should get below response :
{
"timestamp": 1557753285553,
"status": 403,
"error": "Forbidden",
"message": "Invalid X-COMPANY-ID and/or X-OPERATOR-ID are passed. Please validate the request.",
"path": "/apa/invoices"
}
But instead of this i am getting below error :
{
"status": 401,
"message": "Full authentication is required to access this resource"
}
I'm trying to add authentication layer into my spring boot application, and all the tutorials i can find are with some mock user, such as:
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("learn").password("share").roles("USER");
}
I would like to implement something like this:
String credentials = request.getHeader('Authorization');
String userName = fetchUserName(credentials);
String password = fetchUserPassword(credentials);
String actualPassword = redis.getPassowrdForUser(userName);
if (actualPassword == someHash(password)) {
// continue with the request
} else {
// return unauthorised request 401
}
Thanks.
This is my current implementation, from some tutorial i've found:
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private BasicAuthenticationPoint authEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/signup").permitAll()
.anyRequest().authenticated();
http.httpBasic().authenticationEntryPoint(authEntryPoint);
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("learn").password("share").roles("USER");
}
#Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
#Component
public class BasicAuthenticationPoint extends BasicAuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
throws IOException, ServletException {
response.addHeader("WWW-Authenticate", "Basic realm=" +getRealmName());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("HTTP Status 401 - " + authEx.getMessage());
}
#Override
public void afterPropertiesSet() throws Exception {
setRealmName("learn");
super.afterPropertiesSet();
}
}
I need to implement authorization with a specific header (say "sessionId") and secure all uri's except one.
I extended OncePerRequestFilter and implemented custom AuthenticationProvider to check if sessionId is valid (as well as custom Token class etc).
How it works now: for any uri it immediately jumps to AuthSessionAuthenticationProvider's authenticate method right after AuthSessionFilter is applied and returns 403 if header sessionId isn't specified. But I want some uri's to allow access without that header.
It all together:
config:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(permittedUris).permitAll()
.anyRequest().authenticated()
.and().exceptionHandling().accessDeniedHandler(new AuthSessionAccessDeniedHandler())
.and().addFilterBefore(new AuthSessionFilter(), BasicAuthenticationFilter.class);
}
Filter:
public class AuthSessionFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication auth = new AuthSessionToken(request.getHeader("sessionId"));
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
}
}
Provider:
public class AuthSessionAuthenticationProvider implements AuthenticationProvider {
//...
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AuthSessionToken token = (AuthSessionToken) authentication;
if (token.getSessionId() == null) {
throw new AccessDeniedException("Missing header sessionId");
}
AuthSessionAuthorities user = authSessionService.getUserAuthoritiesToken(token.getSessionId());
if (user == null) {
throw new AccessDeniedException("Session ID invalid: " + token.getSessionId());
}
token.setAuthenticatedUser(user);
return token;
}
//...
}
I found more elegant solution that was developed exactly for that purpose.
It's a RequestHeaderAuthenticationFilter. And then antMatchers works as expected. The initial configuration looks like this:
#Bean
#SneakyThrows
public RequestHeaderAuthenticationFilter preAuthenticationFilter() {
RequestHeaderAuthenticationFilter preAuthenticationFilter = new RequestHeaderAuthenticationFilter();
preAuthenticationFilter.setPrincipalRequestHeader(SESSION_ID);
preAuthenticationFilter.setCredentialsRequestHeader(SESSION_ID);
preAuthenticationFilter.setExceptionIfHeaderMissing(false);
preAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(true);
preAuthenticationFilter.setAuthenticationManager(authenticationManager());
return preAuthenticationFilter;
}
I have a question regarding security implementation on my server. I am making a SpringBoot application which has a control panel like website on it, where 1 single admin inputs needed data and i have managed to secure that part fine like this :
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/*").authorizeRequests().anyRequest().hasRole("ADMIN")
.and().formLogin().loginPage("/login.jsp")
.failureUrl("/login.jsp?error=1").loginProcessingUrl("/login")
.permitAll().and().logout()
.logoutSuccessUrl("/login.jsp");
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Create a default account
auth.inMemoryAuthentication()
.withUser("admin")
.password("admin")
.roles("ADMIN");
}
Every website url is on /*, and that works fine. The next thing i need to do is to retrieve data from my mobile app and it needs to be secure. urls that the app should use is /rest/**. I have a Student class that stores email(username) and password that is created by that admin on web site. As far as i've read i need token implementation.
How can I implement token authentication?
To implement token based authentication for a mobile app, with Spring Boot and Spring Security.
Create a TokenAuthenticationFilter
public class TokenAuthenticationFilter extends GenericFilterBean {
private AuthenticationManager authenticationManager;
public TokenAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String apiKey = httpRequest.getHeader("API-Key");
String token = httpRequest.getHeader("Access-Token");
try {
if (!StringUtils.isEmpty(apiKey)) {
processTokenAuthentication(apiKey);
}
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException internalAuthenticationServiceException)
{
SecurityContextHolder.clearContext();
logger.error("Internal authentication service exception", internalAuthenticationServiceException);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
catch(AuthenticationException authenticationException)
{
SecurityContextHolder.clearContext();
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
private void processTokenAuthentication(String apiKey) {
SessionCredentials authCredentials = new SessionCredentials(apiKey);
Authentication requestAuthentication = new PreAuthenticatedAuthenticationToken(authCredentials, authCredentials);
Authentication resultOfAuthentication = tryToAuthenticate(requestAuthentication);
SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication);
}
private Authentication tryToAuthenticate(Authentication requestAuthentication) {
Authentication responseAuthentication = authenticationManager.authenticate(requestAuthentication);
if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) {
throw new InternalAuthenticationServiceException("Unable to authenticate Domain User for provided credentials");
}
return responseAuthentication;
}
}
public class TokenAuthenticationProvider implements AuthenticationProvider {
private String apiKey;
public TokenAuthenticationProvider(String apiKey) {
this.apiKey = apiKey;
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SessionCredentials credentials = (SessionCredentials) authentication.getCredentials();
if (credentials != null && credentials.apiKey.equals(this.apiKey)) {
//Also evaluate the token here
Authentication newAuthentication = new PreAuthenticatedAuthenticationToken(apiKey, credentials);
newAuthentication.setAuthenticated(true);
return newAuthentication;
}
throw new BadCredentialsException("Bad credentials given.");
}
#Override
public boolean supports(Class<?> aClass) {
return aClass.equals(PreAuthenticatedAuthenticationToken.class);
}
}
Create Session Credentials Holder
public class SessionCredentials {
String apiKey;
String accessToken;
public SessionCredentials(String apiKey, String accessToken) {
this.apiKey = apiKey;
this.accessToken = accessToken;
}
public String getApiKey() {
return apiKey;
}
public String getAccessToken() {
return accessToken;
}
}
Finally Register These in your Security Config
//Leave whatever you had here
#Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new TokenAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class);
String contentPathDir = String.format("/%s/**", contentPath);
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
.antMatchers("/authorization/**", "/public/**", "/management/**", "/health/**", contentPathDir).permitAll()
.antMatchers("/**").authenticated();
}
//Add these two below.
#Override
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(apiKeyAuthenticationProvider());
}
#Bean
public TokenAuthenticationProvider apiKeyAuthenticationProvider() {
return new TokenAuthenticationProvider(apiKey);
}
Why the login isn't prompted with following configuration? When I try to access /public/user, I get error 403 (access denied). However, if I uncomment those commented lines at WebServiceSecurityConfiguration.configure, I got redirected to login page, as desired. Why those lines are needed for from-login being properly configured, as the antMatcher matches different path in the first place. I guess there is some conflict, which misconfigures the AuthenticationEntryPoint, but I don't really have idea how that happens. What I'm trying to achieve is configuring two security chains, one for login path to obtain the JWT token, and another for web services to authenticate against the token. Everything works perfectly with those lines uncommented, but I noticed by accident form-login stopped working without them, and am super confused why is that.
#Configuration
#Profile("javasecurity")
#Order(11)
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private TokenHandler tokenHandler;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("password").authorities(new SimpleGrantedAuthority("ROLE_USER")).and()
.withUser("admin").password("password").authorities(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("ROLE_ADMIN")).and()
.withUser("guest").password("guest").authorities(new SimpleGrantedAuthority("ROLE_GUEST"));
}
#Override
#Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**")
.permitAll()
.and()
.formLogin()
.successHandler(authenticationSuccessHandler())
.and()
.logout();
}
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new AuthenticationSuccessHandler() {
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
tokenHandler.setToken(response, authentication.getName());
response.getWriter().println("User authenticated and cookie sent");
response.flushBuffer();
}
};
}
#Configuration
#Profile("javasecurity")
#Order(10)
public static class WebServiceSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private TestAuthenticationFilter testAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/secured/**")
.authenticated();
// .and()
// .antMatcher("/secured/**")
// .securityContext().securityContextRepository(new NullSecurityContextRepository())
// .and()
// .addFilterAt(testAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
--
#Component("TestAuthenticationFilter")
public class TestAuthenticationFilter extends GenericFilterBean {
#Autowired
private TokenHandler tokenHandler;
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("TestAuthenticationFilter doFitler");
attemptAuthentication((HttpServletRequest) request);
chain.doFilter(request, response);
clearAuthentication();
System.out.println("doFitler end");
}
public void attemptAuthentication(HttpServletRequest request) {
try {
UserDetails user = tokenHandler.loadUserFromToken(request);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, user.getPassword());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
// Do nothing
}
}
public void clearAuthentication() {
SecurityContextHolder.getContext().setAuthentication(null);
}
#Configuration
public static class DisableFilterRegistration {
#Autowired
private TestAuthenticationFilter filter;
#Bean
public FilterRegistrationBean disablerBean() {
FilterRegistrationBean bean = new FilterRegistrationBean(filter);
bean.setEnabled(false);
return bean;
}
}
}
--
#Component("TokenHandler")
public class TokenHandler {
#Autowired(required = false)
private UserDetailsService userDetailsService;
public void setToken(HttpServletResponse response, String username) {
response.addCookie(new Cookie("user", username));
}
public UserDetails loadUserFromToken(HttpServletRequest request) throws BadCredentialsException {
Cookie[] cookies = request.getCookies();
Cookie token = null;
for (Cookie c : cookies) {
if (c.getName().equals("user")) {
token = c;
break;
}
}
if (token == null)
return null;
else
return userDetailsService.loadUserByUsername(token.getValue());
}
}
--
#RestController
#RequestMapping("/public")
public class PublicController {
#GetMapping("/norole")
public String noRole() {
return "no role";
}
#GetMapping("/user")
#PreAuthorize("hasRole('ROLE_USER')")
public String roleUser() {
return "role_user";
}
}
--
#RestController
#RequestMapping("/secured")
public class SecuredController {
#GetMapping("/user")
#PreAuthorize("hasRole('ROLE_USER')")
public String roleUser() {
return "role_user";
}
#GetMapping("/admin")
#PreAuthorize("hasRole('ROLE_ADMIN')")
public String roleAdmin() {
return "role_admin";
}
#GetMapping("/norole")
public String noRole() {
return "no role";
}
}
Login-from got functional again after declaring adding
http.antMatcher("/secured/**")
As the first call in the WebServiceSecurityConfiguration.configure. Does that mean that without it the configuration negates the form-login, which is configured after this particular configuration? Also, it seems like the position of antMatcher can be arbitrary, is this the case? Could someone explain what is actually happening there?