I have setup a Spring boot application running in tomcat to be distributed as WAR to server
I have several HTML pages that does work with form security but I am trying to add a API also therefore I am switching to JWT.
My effort is then to combine local client with back end API in one WAR file as I happen to know it is possible with Spring Security
EDIT: My gradle
plugins {
id 'org.springframework.boot' version '2.2.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
id 'war'
}
....
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.auth0:java-jwt:3.8.3'
in my SecurityConfiguration.java I have
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// remove csrf and state in session because in jwt we do not need them
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// add jwt filters (1. authentication, 2. authorization)
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager(), this.userRepository))
.authorizeRequests()
// configure access rules
.antMatchers("/index.html").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers("/api/public/management/*").hasRole("MANAGER")
.antMatchers("/api/public/admin/*").hasRole("ADMIN")
.anyRequest().authenticated();
}
EDIT: Added the JwtAuthenticationFilter
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/* Trigger when we issue POST request to /login
We also need to pass in {"username":"admin", "password":"password"} in the request body
*/
#Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException {
// Grab credentials and map them to LoginViewModel
LoginViewModel credentials = null;
try {
credentials = new ObjectMapper().readValue(request.getInputStream(), LoginViewModel.class);
} catch (IOException e) {
e.printStackTrace();
}
// Create login token
assert credentials != null;
System.out.println("Credentials : " + credentials.getUsername() + ":" + credentials.getPassword());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
credentials.getUsername(),
credentials.getPassword(),
new ArrayList<>()
);
// Authenticate user
return authenticationManager.authenticate(authenticationToken);
}
#Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult
) {
// Grab principal
UserPrincipal principal = (UserPrincipal) authResult.getPrincipal();
// Create JWT Token
String token = JWT.create()
.withSubject(principal.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.sign(HMAC512(SECRET.getBytes()));
System.out.println("Token : " + TOKEN_PREFIX + token);
// Add token in response
response.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
EDIT: Added JwtAuthorizationFilter
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private UserRepository userRepository;
JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
super(authenticationManager);
this.userRepository = userRepository;
}
#Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
// Read the Authorization header, where the JWT token should be
String header = request.getHeader(HEADER_STRING);
// If header does not contain BEARER or is null delegate to Spring impl and exit
if (header == null || !header.startsWith(TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// If header is present, try grab user principal from database and perform authorization
Authentication authentication = getUsernamePasswordAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
// Continue filter execution
chain.doFilter(request, response);
}
private Authentication getUsernamePasswordAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING)
.replace(TOKEN_PREFIX, "");
// parse the token and validate it
String userName = JWT.require(HMAC512(SECRET.getBytes()))
.build()
.verify(token)
.getSubject();
// Search in the DB if we find the user by token subject (username)
// If so, then grab user details and create spring auth token using username, pass, authorities/roles
if (userName != null) {
System.out.println("userName :" + userName);
User user = userRepository.findByUsername(userName);
UserPrincipal principal = new UserPrincipal(user);
return new UsernamePasswordAuthenticationToken(userName, null, principal.getAuthorities());
}
return null;
}
Normally I could just execute the index.html from the browser by running https://localhost:8443/index
It does give me an error
There was an unexpected error (type=Forbidden, status=403). Access
Denied
HOWEVER if i test it in Postman WITH a Bearer token It serves the page
My question is how to exclude the HTML content from the JwtAuthenticationFilter and how to actually do I authorize my HTML content?
My thoughts is to combine formLogin() with JWT but I cannot find sample code to teach me.
Please ask any other code part if you need
It's hard to say exactly what's going on with so much custom code, but here are some tips based on your explanation.
Possibility #1
First, your JwtAuthenticationFilter or your JwtAuthorizationFilter may be expecting too much. Do they try and authentication and authorize the request when there is no bearer token in the request?
A good way to simplify this is to upgrade to Spring Security 5.1+ and use its bearer token support:
http
.authorizeRequests()
.antMatchers("/index.html").permitAll()
... // other matchers
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
It only activates when there is a bearer token present. Also, it disables CSRF for you for requests that contain a bearer token.
You can add formLogin(), but it is a bit at odds with sessionCreationPolicy(STATELESS). Form login needs a sessioned web application. If you want a username/password client that sends its credentials on every request, then you need httpBasic() instead, which you can do like so:
http
.authorizeRequests()
.antMatchers("/index.html").permitAll()
... // other matchers
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.oauth2ResourceServer()
.jwt();
With this setup, httpBasic will engage only when using Authorization: Basic and oauth2ResourceServer will engage only when using Authorization: Bearer.
If you really are wanting formLogin (a UI where a user can fill out an HTML form), then you don't want the application to be stateless. You can do:
http
.authorizeRequests()
.antMatchers("/index.html").permitAll()
... // other matchers
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.oauth2ResourceServer()
.jwt();
Possibility #2
Or, second, you might say that the URIs for your API are completely separate from your client application.
For example, URIs for your client app all start with /app.
And URIs for your API all start with /api.
If that's the case, then you can create two instances of WebSecurityConfigurerAdapter:
#Configuration
#Order(100)
public class AppConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/app/**")
.and()
.authorizeRequests()
.antMatchers("/app/index.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
#Configuration
#Order(101)
public class ApiConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) {
http
.requestMatchers()
.antMatchers("/api/**")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.addFilter(new JwtAuthenticationFilter(...))
.addFilter(new JwtAuthorizationFilter(...))
.csrf().disable();
}
}
Note that both of these setups acknowledge that you don't want to disable CSRF for your entire application. You want that protection on for the client app.
After investigating I found the answer
Thank you #jzheaux for your Input it was instrumental in clarifying what I need to do.
I needed to split the API from the WebClient and have JWT on the Api part but FormLogin on the WebClient part.
It is is probably possible to to combine the two but my experience with coding is not there yet
Related
I want to add simple config for basic authentication using spring security InMemoryUserDetailsManager
After adding following configuration I am able to authenticate with the in memory user (myUser) and the password for this user:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#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(inMemoryUserDetailsManager());
}
#Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
List<UserDetails> userDetailsList = new ArrayList<>();
userDetailsList.add(User.withUsername("myUser").password(passwordEncoder().encode("password"))
.roles("USER").build());
return new InMemoryUserDetailsManager(userDetailsList);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
The thing is that if I change the password from postman I am still able to authenticate. If I stop application server and start the application again and try with wrong password and correct username it returns 401 ( which is expected). However if next request is sent with the correct header with username and password (myUser, password) and then send the request after that with wrong password it seems the wrong password is accepted. As soon as I change the username to some random word it returns 401 unauthorized. Something is missing from my configuration and I do not have a clue what is it.
Spring by default stores the HttpSession of the Authentication details. So whenever user logs in and authentication is successful, the details are stores in ThreadLocal and whenever the next login happens, it picks it up from the security context instead of authenticating again. Spring Security provides multiple Policies for Session Management. For your use case, you need to configure your HttpSecurity with SessionCreationPolicy.STATELESS.
http
.csrf()
.disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
You can also refer the below article for detailed information:
https://www.javadevjournal.com/spring-security/spring-security-session/
I'm facing problem with enabling user log-in page - 404 not found.
This is tutorial that I'm using as base of my application security.
That's how configure function looks like:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// this disables session creation on Spring Security
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
I have tried to simply add here:
.and()
.formLogin()
.loginPage("/login")
.permitAll();
and changing .addFilter to .addFilterAfter() i still get 404.
As you can see in the tutorial, that's how login function is accessed:
curl -i -H "Content-Type: application/json" -X POST -d '{
"username": "admin",
"password": "password"
}' http://localhost:8080/login
Is it even possible to enable built-in login form for this purpose?
And if not, what's the sollution there? Do i have to create /login endpoint in controller, and then POST data to http://localhost:8080/login?
And what about added authenticationFilter. Does changes have to be made there?
You have two problems here:
JWTAuthenticationFilter extends from UsernamePasswordAuthenticationFilter which by default responds to the URL /login. formLogin() also generates a login form in this URL. So, you have two places accepting input for /login. If you choose to do a custom login page (by .loginPage("/login") ) you have to do this in a different URL, and provide the HTML view to this page. But you said that you wanted to use the built-in login form. So, here comes another problem:
To use built-in login form it has to be done by the default /login URL, so you have to change de URL of JWTAuthenticationFilter. It can be achieved by setting a custom URL in AbstractAuthenticationProcessingFilter as saw here. This works like a charm, but the implementation from JWTAuthenticationFilter is expecting as input an JSON, which is not provided by /login form (it send parameters in POST). So you have to change the code for JWTAuthenticationFilter.attemptAuthentication to decide if the input comes from a JSON body or parameters.
I implemented this in my environment and worked great.
Below is the code (just the snippets):
WebSecurity:
public JWTAuthenticationFilter getJWTAuthenticationFilter() throws Exception {
final JWTAuthenticationFilter filter = new JWTAuthenticationFilter(authenticationManager());
filter.setFilterProcessesUrl("/api/auth/login");
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll()
.anyRequest().authenticated()
.and()
.addFilter(getJWTAuthenticationFilter())
.addFilter(new JWTAuthorizationFilter(authenticationManager())) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginProcessingUrl("/api/auth/login")
.permitAll();
}
JWTAuthenticationFilter:
#Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
ApplicationUser creds = null;
if (req.getParameter("username") != null && req.getParameter("password") != null) {
creds = new ApplicationUser();
creds.setUsername(req.getParameter("username"));
creds.setPassword(req.getParameter("password"));
} else {
creds = new ObjectMapper()
.readValue(req.getInputStream(), ApplicationUser.class);
}
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
I have SpringSecurityWebAppConfig class that uses my own filter class JwtAuthenticationFilter.
The question now is how do i bypass my JwtAuthenticationFilter for api calls that does not have request header and/or token. Do I set it as a configurable and read it in the filter?
My JwtAuthenticationFilter is an imported class from another library. The purpose is to reuse the file for the other microservices.
Example:
/scanFile does not need request token. When I add into SpringSecurityWebAppConfig. My filter is throw 401 as it does not have request token.
SpringSecurityWebAppConfig class:
public class SpringSecurityWebAppConfig extends WebSecurityConfigurerAdapter {
#Autowired
public JwtAuthenticationFilter jwtAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/homePage").access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
.antMatchers("/userPage").access("hasRole('ROLE_USER')")
.antMatchers("/adminPage").access("hasRole('ROLE_ADMIN')")
.antMatchers(HttpMethod.DELETE, "/data").access("hasRole('ROLE_ADMIN')")
.antMatchers("/login").permitAll()
.antMatchers("/logout").authenticated()
.and()
.anonymous().disable()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.headers()
.httpStrictTransportSecurity()
.includeSubDomains(true).maxAgeInSeconds(31536000);
// Add a filter to validate the tokens with every request
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JwtAuthenticationFilter Class:
private void getJwtFromRequest(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String bearerToken = request.getHeader("Authorization");
// Step 1: Check if bearer token exist in authorization header and if bearer token start with "Bearer "
if (!StringUtils.hasText(bearerToken) || !bearerToken.startsWith("Bearer ")) {
String errorMsg = "No access token found in request headers.";
Error err = new Error(JWT_AUTHENTICATION_FILTER, "AccessTokenMissingException", errorMsg);
// Java object to JSON string
String jsonString = mapper.writeValueAsString(err);
log.error(jsonString);
throw new AccessTokenMissingException(errorMsg);
}
//rest of the processing here ...
}
so you want to whitelist some apis even without spring security authentication?
you can use web.ignoring().antMatchers() in your SpringSecurityWebAppConfig, to exclude the urls from spring security.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/scanFile/**”);
}
I am wondering if there is a way to provide two separate types of authentication?
User should log, register, get user data for endpoints /login, /register, /user using basic auth. And when I call /api it should only be authenticated with JWT token provided in headers.
But when I call /api I get all data without any authentication. When user is logged and call /user, API gives JWT to access /api.
My code:
Configuration for basic auth:
#Configuration
#EnableWebSecurity
#Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable();
http
.authorizeRequests()
.antMatchers("/user").authenticated()
.antMatchers("/register").permitAll()
.and()
.formLogin().permitAll()
.defaultSuccessUrl("/user");
}
Configuration for JWT auth:
#Configuration
#Order(2)
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}
I had the same problem, I wanted basic Authentication for some endpoints and for some other I wanted other authentication methods. like yours. you wanna basic authentication for some of the endpoints (/login,/register, /user ) and JWT authentication for some other(/api/**).
I used some tutorials about multiple entry points in spring security but it didn't work.
So here is my solution (It worked)
Separate basic authentication from JWT authentication by creating a custom filter.
Add a prefix path for the endpoints that should be authenticated using basic authentication. like :
(/basic/login, /basic/register,/basic/user)
Create a new custom filter for /basic prefix (for /basic requests) and check basic authentication
#Component
public class BasicAuthenticationFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
//Check for the requests that starts with /basic
if (httpServletRequest.getRequestURI().startsWith("/basic/")) {
try {
//Fetch Credential from authorization header
String authorization = httpServletRequest.getHeader("Authorization");
String base64Credentials = authorization.substring("Basic".length()).trim();
byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
String credentials = new String(credDecoded, StandardCharsets.UTF_8);
final String username = credentials.split(":", 2)[0];
final String password = credentials.split(":", 2)[1];
//Check the username and password
if (username.equals("admin") && password.equals("admin")) {
//continue
chain.doFilter(request, response);
} else
throw new AuthenticationCredentialsNotFoundException("");
} catch (Exception e) {
throw new AuthenticationCredentialsNotFoundException("");
}
} else chain.doFilter(request, response);
}
}
Write main security configuration just for JWT and permit /basic URL
#Configuration
#EnableWebSecurity
public class JWTSecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/basic/**").permitAll().and()
.csrf().disable()
.antMatcher("/api/**")
.addFilterAfter(new JWTAuthorizationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic().disable();
}
Login is success but spring security blocking url even i given access to USER . How can i manage this thing?
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().withUser("sahil").password("123")
.roles("ADMIN","USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/welcome","/inventory/**","/sales/**").access("hasRole('USER')")
.and()
.csrf().disable();
}
LoginController.java
#Controller
public class LoginController {
#RequestMapping(value = { "/", "/login" }, method = RequestMethod.GET)
public String showLoginPage() {
return "login";
}
#RequestMapping(value = "/login", method = RequestMethod.POST)
public String handleUserLogin(ModelMap model, #RequestParam String name, #RequestParam String password) {
if (!service.validateUser(name, password)) {
model.put("errorMsg", "Invalid Credential");
return "login";
}
System.out.println("principal : " + getLoggedInUserName());
model.put("name", name);
model.put("password", password);
return "welcome";
}
private String getLoggedInUserName() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
System.out.println("in if");
return ((UserDetails)principal).getUsername();
} else {
System.out.println("in else");
return principal.toString();
}
}
#RequestMapping(value = "/welcome", method = RequestMethod.GET)
public String showWelcomeDashboard() {
return "welcome";
}
}
1 . Once Login success page redirected to welcome page but url is still localhost:8080/login instead of localhost:8080/welcome.
2. After redirecting to URL localhost:8080/sales is it 403 Access denied.
What is spring security
Spring security is all about authentication and authorization, in your case you are missing authentication. There is no configuration of authentication in your security configuration. What you are missing is authentication filter for your spring security. Spring security provides default authentication filter UsernamePasswordAuthenticationFilter that can be configured by .formLogin(). You can use default provided or you can define your own custom authentication filter(Implementation of UsernamePasswordAuthenticationFilter).
Once authentication is success spring security will grant authorities for authenticated user. If authentication is configured correctly, below configuration is responsible for authentication and granting authority
auth.inMemoryAuthentication().withUser("sahil").password("123")
.roles("ADMIN","USER");
Authenticated users each request will be passed through filter FilterSecurityInterceptor and it will verifies authority granted for authenticated user with authorization configured for resources as given in below code.
.antMatchers("/welcome","/inventory/**","/sales/**").access("hasRole('USER')")
You missed all this by not configuring authentication filter.
Now for making it simple use.formLogin() in your http configuration.
#Override
protected void configure(final HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers("/welcome","/inventory/**","/sales/**").access("hasRole('USER')")
.and().exceptionHandling()
.accessDeniedPage("/403")
.and().formLogin()
.and().logout()
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.and()
.csrf()
.disable();
}
.formLogin() without any configuration provides default login page with username and password default form parameters.And after authentication it redirects to "/" If you want to provide your custom login page then use below configuration.
.and().formLogin()
.loginPage("/login")
.usernameParameter("email").passwordParameter("password")
.defaultSuccessUrl("/app/user/dashboard")
.failureUrl("/login?error=true")
.loginPage("") - Your custom login page URL
.usernameParameter("").passwordParameter("") - Your custom login form parameters
.defaultSuccessUrl("") - Page url after successful authentication
.failureUrl("") - Page url after authentication failure
Note: You should not use "/login" POST method in your controller, Even though if you write, it will not be reached from spring security filter chain. As your configuration was wrong before, it was reaching before! Now you remove those from your controller and use conventional approach as mentioned above.